/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Globalization;
using System.Text;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;

namespace Xceed.Silverlight.ListBox.Converters
{
  // Documentation on Path Mini-Language: http://msdn.microsoft.com/en-us/library/ms752293.aspx
  // This source code obtained from Codeplex and the URL is http://stringtopathgeometry.codeplex.com/.
  public class StringToPathGeometryConverter : IValueConverter
  {
    #region Private Methods

    private PathGeometry Parse( string path )
    {
      PathGeometry pathGeometry = null;
      m_formatProvider = CultureInfo.InvariantCulture;
      m_pathString = path;
      m_pathLength = path.Length;
      m_curIndex = 0;

      m_secondLastPoint = new Point( 0, 0 );
      m_lastPoint = new Point( 0, 0 );
      m_lastStart = new Point( 0, 0 );

      m_figureStarted = false;

      bool first = true;

      char last_cmd = ' ';

      while( this.ReadToken() ) // Empty path is allowed in XAML
      {
        char cmd = m_token;

        if( first )
        {
          if( ( cmd != 'M' ) && ( cmd != 'm' ) )  // Path starts with M|m 
          {
            this.ThrowBadToken();
          }

          first = false;
        }

        switch( cmd )
        {
          // Start Point of a Path
          case 'm':
          case 'M':
            // XAML allows multiple points after M/m
            m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );

            m_figure = new PathFigure();
            m_figure.StartPoint = m_lastPoint;
            m_figure.IsFilled = StringToPathGeometryConverter.IsFilled;
            m_figure.IsClosed = !StringToPathGeometryConverter.IsClosed;
            //context.BeginFigure(_lastPoint, IsFilled, !IsClosed);
            m_figureStarted = true;
            m_lastStart = m_lastPoint;
            last_cmd = 'M';

            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) )
            {
              m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );

              LineSegment lineSegment = new LineSegment();
              lineSegment.Point = m_lastPoint;
              m_figure.Segments.Add( lineSegment );
              last_cmd = 'L';
            }
            break;

          case 'l':
          case 'L':
          case 'h':
          case 'H':
          case 'v':
          case 'V':
            this.EnsureFigure();

            do
            {
              switch( cmd )
              {
                // Line: endPoint
                case 'l':
                case 'L':
                  m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  break;
                // Horizontal: X
                case 'h':
                case 'H':
                  m_lastPoint.X = this.ReadNumber( !StringToPathGeometryConverter.AllowComma );
                  break;
                // Vertical: Y
                case 'v':
                case 'V':
                  m_lastPoint.Y = this.ReadNumber( !StringToPathGeometryConverter.AllowComma );
                  break;
              }

              LineSegment lineSegment = new LineSegment();
              lineSegment.Point = m_lastPoint;
              m_figure.Segments.Add( lineSegment );
            }
            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) );

            last_cmd = 'L';
            break;

          // Cubic Bezier: controlPoint1 controlPoint2 endPoint
          case 'c':
          case 'C':
          // Smooth cublic Bezier: controlPoint2 endPoint
          case 's':
          case 'S':
            this.EnsureFigure();

            do
            {
              Point point;

              switch( cmd )
              {
                case 's':
                case 'S':
                  if( last_cmd == 'C' )
                  {
                    point = this.Reflect();
                  }
                  else
                  {
                    point = m_lastPoint;
                  }

                  m_secondLastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  break;

                default:
                  point = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );

                  m_secondLastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );
                  break;
              }

              m_lastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );

              BezierSegment bezierSegment = new BezierSegment();
              bezierSegment.Point1 = point;
              bezierSegment.Point2 = m_secondLastPoint;
              bezierSegment.Point3 = m_lastPoint;
              m_figure.Segments.Add( bezierSegment );

              last_cmd = 'C';
            }
            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) );

            break;

          // Quadratic Bezier: controlPoint endPoint
          case 'q':
          case 'Q':
          // Smooth Quadratic Bezier: controlPoint endPoint
          case 't':
          case 'T':
            this.EnsureFigure();

            do
            {
              switch( cmd )
              {
                case 't':
                case 'T':
                  if( last_cmd == 'Q' )
                  {
                    m_secondLastPoint = this.Reflect();
                  }
                  else
                  {
                    m_secondLastPoint = m_lastPoint;
                  }

                  m_lastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  break;

                default:
                  m_secondLastPoint = this.ReadPoint( cmd, !StringToPathGeometryConverter.AllowComma );
                  m_lastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );
                  break;
              }

              QuadraticBezierSegment quadraticBezierSegment = new QuadraticBezierSegment();
              quadraticBezierSegment.Point1 = m_secondLastPoint;
              quadraticBezierSegment.Point2 = m_lastPoint;
              m_figure.Segments.Add( quadraticBezierSegment );

              last_cmd = 'Q';
            }
            while( this.IsNumber( AllowComma ) );

            break;

          // Elliptical Arc: size rotationAngle isLargeArcFlag sweepDirectionFlag endPoint
          case 'a':
          case 'A':
            this.EnsureFigure();

            do
            {
              // A 3,4 5, 0, 0, 6,7
              double w = this.ReadNumber( !StringToPathGeometryConverter.AllowComma );
              double h = this.ReadNumber( AllowComma );
              double rotation = this.ReadNumber( StringToPathGeometryConverter.AllowComma );
              bool large = this.ReadBool();
              bool sweep = this.ReadBool();

              m_lastPoint = this.ReadPoint( cmd, StringToPathGeometryConverter.AllowComma );

              ArcSegment arcSegment = new ArcSegment();
              arcSegment.Point = m_lastPoint;
              arcSegment.Size = new Size( w, h );
              arcSegment.RotationAngle = rotation;
              arcSegment.IsLargeArc = large;
              arcSegment.SweepDirection = sweep ? SweepDirection.Clockwise : SweepDirection.Counterclockwise;
              m_figure.Segments.Add( arcSegment );
            }
            while( this.IsNumber( StringToPathGeometryConverter.AllowComma ) );

            last_cmd = 'A';
            break;

          // Close command
          case 'z':
          case 'Z':
            this.EnsureFigure();
            m_figure.IsClosed = StringToPathGeometryConverter.IsClosed;

            m_figureStarted = false;
            last_cmd = 'Z';

            m_lastPoint = m_lastStart; // Set reference point to be first point of current figure
            break;

          default:
            this.ThrowBadToken();
            break;
        }
      }

      if( null != m_figure )
      {
        pathGeometry = new PathGeometry();
        pathGeometry.Figures.Add( m_figure );

      }
      return pathGeometry;
    }

    private void SkipDigits( bool signAllowed )
    {
      // Allow for a sign 
      if( signAllowed && this.More() && ( ( m_pathString[ m_curIndex ] == '-' ) || m_pathString[ m_curIndex ] == '+' ) )
      {
        m_curIndex++;
      }

      while( this.More() && ( m_pathString[ m_curIndex ] >= '0' ) && ( m_pathString[ m_curIndex ] <= '9' ) )
      {
        m_curIndex++;
      }
    }

    private bool ReadBool()
    {
      this.SkipWhiteSpace( StringToPathGeometryConverter.AllowComma );

      if( this.More() )
      {
        m_token = m_pathString[ m_curIndex++ ];

        if( m_token == '0' )
        {
          return false;
        }
        else if( m_token == '1' )
        {
          return true;
        }
      }

      this.ThrowBadToken();

      return false;
    }

    private Point Reflect()
    {
      return new Point( 2 * m_lastPoint.X - m_secondLastPoint.X,
                       2 * m_lastPoint.Y - m_secondLastPoint.Y );
    }

    private void EnsureFigure()
    {
      if( !m_figureStarted )
      {
        m_figure = new PathFigure();
        m_figure.StartPoint = m_lastStart;

        //_context.BeginFigure(_lastStart, IsFilled, !IsClosed);
        m_figureStarted = true;
      }
    }

    private double ReadNumber( bool allowComma )
    {
      if( !this.IsNumber( allowComma ) )
      {
        this.ThrowBadToken();
      }

      bool simple = true;
      int start = m_curIndex;

      //
      // Allow for a sign
      //
      // There are numbers that cannot be preceded with a sign, for instance, -NaN, but it's 
      // fine to ignore that at this point, since the CLR parser will catch this later.
      // 
      if( this.More() && ( ( m_pathString[ m_curIndex ] == '-' ) || m_pathString[ m_curIndex ] == '+' ) )
      {
        m_curIndex++;
      }

      // Check for Infinity (or -Infinity).
      if( this.More() && ( m_pathString[ m_curIndex ] == 'I' ) )
      {
        // 
        // Don't bother reading the characters, as the CLR parser will 
        // do this for us later.
        // 
        m_curIndex = Math.Min( m_curIndex + 8, m_pathLength ); // "Infinity" has 8 characters
        simple = false;
      }
      // Check for NaN 
      else if( this.More() && ( m_pathString[ m_curIndex ] == 'N' ) )
      {
        // 
        // Don't bother reading the characters, as the CLR parser will
        // do this for us later. 
        //
        m_curIndex = Math.Min( m_curIndex + 3, m_pathLength ); // "NaN" has 3 characters
        simple = false;
      }
      else
      {
        SkipDigits( !StringToPathGeometryConverter.AllowSign );

        // Optional period, followed by more digits 
        if( this.More() && ( m_pathString[ m_curIndex ] == '.' ) )
        {
          simple = false;
          m_curIndex++;
          SkipDigits( !StringToPathGeometryConverter.AllowSign );
        }

        // Exponent
        if( this.More() && ( ( m_pathString[ m_curIndex ] == 'E' ) || ( m_pathString[ m_curIndex ] == 'e' ) ) )
        {
          simple = false;
          m_curIndex++;
          SkipDigits( StringToPathGeometryConverter.AllowSign );
        }
      }

      if( simple && ( m_curIndex <= ( start + 8 ) ) ) // 32-bit integer
      {
        int sign = 1;

        if( m_pathString[ start ] == '+' )
        {
          start++;
        }
        else if( m_pathString[ start ] == '-' )
        {
          start++;
          sign = -1;
        }

        int value = 0;

        while( start < m_curIndex )
        {
          value = value * 10 + ( m_pathString[ start ] - '0' );
          start++;
        }

        return value * sign;
      }
      else
      {
        string subString = m_pathString.Substring( start, m_curIndex - start );

        try
        {
          return System.Convert.ToDouble( subString, m_formatProvider );
        }
        catch( FormatException except )
        {
          throw new FormatException( string.Format( "Unexpected character in path '{0}' at position {1}", m_pathString, m_curIndex - 1 ), except );
        }
      }
    }

    private bool IsNumber( bool allowComma )
    {
      bool commaMet = this.SkipWhiteSpace( allowComma );

      if( this.More() )
      {
        m_token = m_pathString[ m_curIndex ];

        // Valid start of a number
        if( ( m_token == '.' ) || ( m_token == '-' ) || ( m_token == '+' ) || ( ( m_token >= '0' ) && ( m_token <= '9' ) )
            || ( m_token == 'I' )  // Infinity
            || ( m_token == 'N' ) ) // NaN 
        {
          return true;
        }
      }

      if( commaMet ) // Only allowed between numbers
      {
        this.ThrowBadToken();
      }

      return false;
    }

    private Point ReadPoint( char cmd, bool allowcomma )
    {
      double x = this.ReadNumber( allowcomma );
      double y = this.ReadNumber( StringToPathGeometryConverter.AllowComma );

      if( cmd >= 'a' ) // 'A' < 'a'. lower case for relative
      {
        x += m_lastPoint.X;
        y += m_lastPoint.Y;
      }

      return new Point( x, y );
    }

    private bool ReadToken()
    {
      this.SkipWhiteSpace( !StringToPathGeometryConverter.AllowComma );

      // Check for end of string 
      if( this.More() )
      {
        m_token = m_pathString[ m_curIndex++ ];

        return true;
      }
      else
      {
        return false;
      }
    }

    private bool More()
    {
      return m_curIndex < m_pathLength;
    }

    // Skip white space, one comma if allowed
    private bool SkipWhiteSpace( bool allowComma )
    {
      bool commaMet = false;

      while( this.More() )
      {
        char ch = m_pathString[ m_curIndex ];

        switch( ch )
        {
          case ' ':
          case '\n':
          case '\r':
          case '\t': // SVG whitespace 
            break;

          case ',':
            if( allowComma )
            {
              commaMet = true;
              allowComma = false; // one comma only
            }
            else
            {
              this.ThrowBadToken();
            }
            break;

          default:
            // Avoid calling IsWhiteSpace for ch in (' ' .. 'z']
            if( ( ( ch > ' ' ) && ( ch <= 'z' ) ) || !Char.IsWhiteSpace( ch ) )
            {
              return commaMet;
            }
            break;
        }

        m_curIndex++;
      }

      return commaMet;
    }

    private void ThrowBadToken()
    {
      throw new FormatException( string.Format( "Unexpected character in path '{0}' at position {1}", m_pathString, m_curIndex - 1 ) );
    }

    private static char GetNumericListSeparator( IFormatProvider provider )
    {
      char numericSeparator = ',';

      // Get the NumberFormatInfo out of the provider, if possible
      // If the IFormatProvider doesn't not contain a NumberFormatInfo, then 
      // this method returns the current culture's NumberFormatInfo. 
      NumberFormatInfo numberFormat = NumberFormatInfo.GetInstance( provider );

      // Is the decimal separator is the same as the list separator?
      // If so, we use the ";". 
      if( ( numberFormat.NumberDecimalSeparator.Length > 0 ) && ( numericSeparator == numberFormat.NumberDecimalSeparator[ 0 ] ) )
      {
        numericSeparator = ';';
      }

      return numericSeparator;
    }

    private string ParseBack( PathGeometry geometry )
    {
      StringBuilder sb = new StringBuilder();
      IFormatProvider provider = new CultureInfo( "en-us" );

      string format = null;

      foreach( PathFigure figure in geometry.Figures )
      {
        sb.Append( "M " + ( ( IFormattable )figure.StartPoint ).ToString( format, provider ) + " " );

        foreach( PathSegment segment in figure.Segments )
        {
          char separator = StringToPathGeometryConverter.GetNumericListSeparator( provider );

          if( segment.GetType() == typeof( LineSegment ) )
          {
            LineSegment lineSegment = segment as LineSegment;

            sb.Append( "L " + ( ( IFormattable )lineSegment.Point ).ToString( format, provider ) + " " );
          }
          else if( segment.GetType() == typeof( BezierSegment ) )
          {
            BezierSegment bezierSegment = segment as BezierSegment;

            sb.Append( String.Format( provider,
                 "C{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "} ",
                 separator,
                 bezierSegment.Point1,
                 bezierSegment.Point2,
                 bezierSegment.Point3
                 ) );
          }
          else if( segment.GetType() == typeof( QuadraticBezierSegment ) )
          {
            QuadraticBezierSegment quadraticBezierSegment = segment as QuadraticBezierSegment;

            sb.Append( String.Format( provider,
                 "Q{1:" + format + "}{0}{2:" + format + "} ",
                 separator,
                 quadraticBezierSegment.Point1,
                 quadraticBezierSegment.Point2 ) );
          }
          else if( segment.GetType() == typeof( ArcSegment ) )
          {
            ArcSegment arcSegment = segment as ArcSegment;

            sb.Append( String.Format( provider,
                 "A{1:" + format + "}{0}{2:" + format + "}{0}{3}{0}{4}{0}{5:" + format + "} ",
                 separator,
                 arcSegment.Size,
                 arcSegment.RotationAngle,
                 arcSegment.IsLargeArc ? "1" : "0",
                 arcSegment.SweepDirection == SweepDirection.Clockwise ? "1" : "0",
                 arcSegment.Point ) );
          }
        }

        if( figure.IsClosed )
          sb.Append( "Z" );
      }

      return sb.ToString();
    }

    #endregion

    #region IValueConverter Methods

    public object Convert( object value, Type targetType, object parameter, CultureInfo culture )
    {
      string path = value as string;

      if( !string.IsNullOrEmpty( path ) )
      {
        return this.Parse( path );
      }
      else
      {
        return null;
      }
    }

    public object ConvertBack( object value, Type targetType, object parameter, CultureInfo culture )
    {
      PathGeometry geometry = value as PathGeometry;

      if( null != geometry )
      {
        return this.ParseBack( geometry );
      }
      else
      {
        return default( string );
      }
    }

    #endregion

    #region Private Fields

    private IFormatProvider m_formatProvider;

    private PathFigure m_figure = null;     // Figure object, which will accept parsed segments
    private string m_pathString;        // Input string to be parsed
    private int m_pathLength;
    private int m_curIndex;          // Location to read next character from
    private bool m_figureStarted;     // StartFigure is effective 

    private Point m_lastStart;         // Last figure starting point
    private Point m_lastPoint;         // Last point 
    private Point m_secondLastPoint;   // The point before last point

    private char m_token;             // Non whitespace character returned by ReadToken 

    #endregion

    #region Private Constants

    private static bool AllowSign = true;
    private static bool AllowComma = true;
    private static bool IsFilled = true;
    private static bool IsClosed = true;

    #endregion
  }
}
